用 SSE 构建实时 Web 应用

在标准的 HTTP 请求-响应的情况下,客户端打开一个连接,发送一个 HTTP请求(例如 HTTP GET 请求)到服务端,然后接收到 HTTP 回来的响应,一旦这个响应完全被发送或者接收,服务端就关闭连接。当客户端需要请求所有数据时,这通常总是由一个客户发起。

12. Build Real-Time Web App with SSE 用 SSE 构建实时 Web 应用 - 图1

相反, Server-Sent Events (SSE) 是一种机制,一旦由客户端建立客户机-服务器的连接,就能让服务端异步地将数据从服务端推到客户端。当连接由客户端建立完成,服务端就提供数据,并决定新数据“块”可用时将其发送到客户端。当一个新的数据事件发生在服务端时,这个事件被服务端发送到客户端。因此,名称被称为 Server-Sent Events(服务器推送事件)。下面是支持服务端到客户端交互的技术总览:

  • 插件提供 socket 方式:比如利用 Flash XMLSocket,Java Applet 套接口,Activex 包装的 socket。

    • 优点:原生 socket 的支持,与 PC 端的实现方式相似;
    • 缺点:浏览器端需要装相应的插件;与 js 进行交互时复杂
  • Polling:轮询,重复发送新的请求到服务端。如果服务端没有新的数据,就发送适当的指示并关闭连接。然后客户端等待一段时间后,发送另一个请求(例如,一秒后)

    • 优点:实现简单,无需做过多的更改
    • 缺点:轮询的间隔过长,会导致用户不能及时接收到更新的数据;轮询的间隔过短,会导致查询请求过多,增加服务器端的负担。

12. Build Real-Time Web App with SSE 用 SSE 构建实时 Web 应用 - 图2

  • Long-polling:长轮询,客户端发送一个请求到服务端,如果服务端没有新的数据,就保持住这个连接直到有数据。一旦服务端有了数据(消息)给客户端,它就使用这个连接发送数据给客户端。接着连接关闭。
    • 优点:比 Polling 做了优化,有较好的时效性
    • 缺点:需第三方库支持,实现较为复杂;每次连接只能发送一个数据,多个数据发送时耗费服务器性能

12. Build Real-Time Web App with SSE 用 SSE 构建实时 Web 应用 - 图3

  • 基于 iframe 及 htmlfile 的流(streaming)方式:iframe 流方式是在页面中插入一个隐藏的 iframe,利用其src属性在服务器和客户端之间创建一条长链接,服务器向 iframe 传输数据(通常是 HTML,内有负责插入信息的 javascript),来实时更新页面。
    • 优点:消息能够实时到达;
    • 缺点:服务器维持着长连接期会消耗资源;iframe 不规范的用法;数据推送过程会有加载进度条显示,界面体验不好

12. Build Real-Time Web App with SSE 用 SSE 构建实时 Web 应用 - 图4

  • Server-Sent events:SSE 与 长轮询机制类似,区别是每个连接不只发送一个消息。客户端发送一个请求,服务端就保持这个连接直到有一个新的消息已经准备好了,那么它将消息发送回客户端,同时仍然保持这个连接是打开,这样这个连接就可以用于另一个可用消息的发送。一旦准备好了一个新消息,通过同一初始连接发送回客户端。客户端单独处理来自服务端传回的消息后不关闭连接。所以,SSE 通常重用一个连接处理多个消息(称为事件)。SSE 还定义了一个专门的媒体类型 text/event-stream,描述一个从服务端发送到客户端的简单格式。SSE 还提供在大多数现代浏览器里的标准 javascript 客户端 API 实现。关于 SSE 的更多信息,请参见 SSE API 规范
    • 优点:HTML5 标准;实现较为简单;一个连接可以发送多个数据
    • 缺点:IE 不支持 EventSource(可以使用第三方的 js 库来解决,具体可以本章中的源码) ;服务器只能单向推送数据到客户端

12. Build Real-Time Web App with SSE 用 SSE 构建实时 Web 应用 - 图5

12. Build Real-Time Web App with SSE 用 SSE 构建实时 Web 应用 - 图6

  • WebSocket: WebSocket 与上述技术都不同,因为它提供了一个真正的全双工连接。发起者是一个客户端,发送一个带特殊 HTTP 头的请求到服务端,通知服务器, HTTP 连接可能“升级”到一个全双工的 TCP/IP WebSocket 连接。如果服务端支持 WebSocket,它可能会选择升级到 WebSocket。一旦建立 WebSocket 连接,它可用于客户机和服务器之间的双向通信。客户端和服务器可以随意向对方发送数据。此时,新的 WebSocket 连接上的交互不再是基于 HTTP 协议了。 WebSocket 可以用于需要快速在两个方向上交换小块数据的在线游戏或任何其他应用程序。(示例可以参考http://www.waylau.com/netty-websocket-chat/)
    • 优点:HTML5 标准;大多数浏览器支持;真正全双工;性能强
    • 缺点:实现相对复杂;ws 协议

12. Build Real-Time Web App with SSE 用 SSE 构建实时 Web 应用 - 图7

12. Build Real-Time Web App with SSE 用 SSE 构建实时 Web 应用 - 图8

SSE vs. WebSocket

概括来说,WebSocket能做的,SSE也能做,反之亦然,但在完成某些任务方面,它们各有千秋。

WebSocket 是一种更为复杂的服务端实现技术,但它是真正的双向传输技术,既能从服务端向客户端推送数据,也能从客户端向服务端推送数据。

WebSocket 和 SSE 的浏览器支持率差不多,除了IE。IE是个例外,即便IE11都还不支持原生 SSE,IE10 添加了WebSocket 支持,可见上图。

与 WebSocket 相比,SSE 有一些显著的优势。我认为它最大的优势就是便利:不需要添加任何新组件,用任何你习惯的后端语言和框架就能继续使用。你不用为新建虚拟机、弄一个新的IP或新的端口号而劳神,就像在现有网站中新增一个页面那样简单。我喜欢把这称为既存基础设施优势。

SSE 的第二个优势是服务端的简洁。我们将在下节中看到,服务端代码只需几行。相对而言,WebSocket 则很复杂,不借助辅助类库基本搞不定。

因为 SSE 能在现有的 HTTP/HTTPS 协议上运作,所以它能直接运行于现有的代理服务器和认证技术。而对 WebSocket 而言,代理服务器需要做一些开发(或其他工作)才能支持,在写这本书时,很多服务器还没有(虽然这种状况会改善)。SSE还有一个优势:它是一种文本协议,脚本调试非常容易。事实上,在本书中,我们会在开发和测试时用 curl,甚至直接在命令行中运行后端脚本。

不过,这就引出了 WebSocket 相较 SSE 的一个潜在优势:WebSocket 是二进制协议,而 SSE 是文本协议(通常使用UTF-8编码)。当然,我们可以通过SSE连接传输二进制数据:在 SSE 中,只有两个具有特殊意义的字符,它们是 CR 和LF,而对它们进行转码并不难。但用 SSE 传输二进制数据时数据会变大,如果需要从服务端到客户端传输大量的二进制数据,最好还是用 WebSocket。

WebSocket 相较 SSE 最大的优势在于它是双向交流的,这意味向服务端发送数据就像从服务端接收数据一样简单。用 SSE时,一般通过一个独立的 Ajax 请求从客户端向服务端传送数据。相对于 WebSocket,这样使用 Ajax 会增加开销,但也就多一点点而已。如此一来,问题就变成了“什么时候需要关心这个差异?”如果需要以1次/秒或者更快的频率向服务端传输数据,那应该用 WebSocket。0.2次/秒到1次/秒的频率是一个灰色地带,用 WebSocket 和用 SSE 差别不大;但如果你期望重负载,那就有必要确定基准点。频率低于0.2次/秒左右时,两者差别不大。

从服务端向客户端传输数据的性能如何?如果是文本数据而非二进制数据(如前文所提到的),SSE和WebSocket没什么区别。它们都用TCP/IP套接字,都是轻量级协议。延迟、带宽、服务器负载等都没有区别。

在旧版本浏览器上的兼容,WebSocket 难兼容,SSE 易兼容。

SSE 的应用场景

看了上述的定义,可以知道 SSE 适合应用于服务端单向推送信息到客户端的场景。 Jersey 的 SSE 大致可以分为发布-订阅模式和广播模式。

为使用 Jersey SSE, 添加如下依赖:

  1. <dependency>
  2. <groupId>org.glassfish.jersey.media</groupId>
  3. <artifactId>jersey-media-sse</artifactId>
  4. </dependency>

发布-订阅模式

服务端代码:

  1. @Path("see-events")
  2. public class SseResource {
  3. private EventOutput eventOutput = new EventOutput();
  4. private OutboundEvent.Builder eventBuilder;
  5. private OutboundEvent event ;
  6. /**
  7. * 提供 SSE 事件输出通道的资源方法
  8. * @return eventOutput
  9. */
  10. @GET
  11. @Produces(SseFeature.SERVER_SENT_EVENTS)
  12. public EventOutput getServerSentEvents() {
  13. // 不断循环执行
  14. while (true) {
  15. SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
  16. String now = df.format(new Date()); //获取当前系统时间
  17. String message = "Server Time:" + now;
  18. System.out.println( message );
  19. eventBuilder = new OutboundEvent.Builder();
  20. eventBuilder.id(now);
  21. eventBuilder.name("message");
  22. eventBuilder.data(String.class,
  23. message ); // 推送服务器时间的信息给客户端
  24. event = eventBuilder.build();
  25. try {
  26. eventOutput.write(event);
  27. } catch (IOException e) {
  28. e.printStackTrace();
  29. } finally {
  30. try {
  31. eventOutput.close();
  32. return eventOutput;
  33. } catch (IOException e) {
  34. e.printStackTrace();
  35. }
  36. }
  37. }
  38. }
  39. }

上面的代码定义了资源部署在 URI “see-events”。这个资源有一个 @GET 资源方法返回作为一个实体 EventOutput ——通用 Jersey ChunkedOutput API 的扩展用于输出分块消息处理。

客户端代码:

  1. //判断浏览器是否支持 EventSource
  2. if (typeof (EventSource) !== "undefined") {
  3. var source = new EventSource("webapi/see-events");
  4. // 当通往服务器的连接被打开
  5. source.onopen = function(event) {
  6. console.log("连接开启!");
  7. };
  8. // 当接收到消息。只能是事件名称是 message
  9. source.onmessage = function(event) {
  10. console.log(event.data);
  11. var data = event.data;
  12. var lastEventId = event.lastEventId;
  13. document.getElementById("x").innerHTML += "\n" + 'lastEventId:'+lastEventId+';data:'+data;
  14. };
  15. //可以是任意命名的事件名称
  16. /*
  17. source.addEventListener('message', function(event) {
  18. console.log(event.data);
  19. var data = event.data;
  20. var lastEventId = event.lastEventId;
  21. document.getElementById("x").innerHTML += "\n" + 'lastEventId:'+lastEventId+';data:'+data;
  22. });
  23. */
  24. // 当错误发生
  25. source.onerror = function(event) {
  26. console.log("连接错误!");
  27. };
  28. } else {
  29. document.getElementById("result").innerHTML = "Sorry, your browser does not support server-sent events..."
  30. }

首先要判断浏览器是否支持 EventSource,而后,EventSource 对象分别监听 onopen、onmessage、onerror 事件。其中, source.onmessage = function(event) {}source.addEventListener('message', function(event) {} 是一样的,区别是,后者可以支持监听不同名称的事件,而 onmessage 属性只支持一个事件处理方法。。

效果

运行项目

  1. mvn jetty:run

浏览器访问 http://localhost:8080

12. Build Real-Time Web App with SSE 用 SSE 构建实时 Web 应用 - 图9

广播模式

服务端代码:

  1. @Singleton
  2. @Path("sse-chat")
  3. public class SseChatResource {
  4. private SseBroadcaster broadcaster = new SseBroadcaster();
  5. /**
  6. * 提供 SSE 事件输出通道的资源方法
  7. * @return eventOutput
  8. */
  9. @GET
  10. @Produces(SseFeature.SERVER_SENT_EVENTS)
  11. public EventOutput listenToBroadcast() {
  12. EventOutput eventOutput = new EventOutput();
  13. this.broadcaster.add(eventOutput);
  14. return eventOutput;
  15. }
  16. /**
  17. * 提供 写入 SSE 事件通道的资源方法
  18. * @param message
  19. * @param name
  20. */
  21. @POST
  22. @Produces(MediaType.TEXT_PLAIN)
  23. public void broadcastMessage(@DefaultValue("waylau.com") @QueryParam("message") String message,
  24. @DefaultValue("waylau") @QueryParam("name") String name) {
  25. SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
  26. String now = df.format(new Date()); //获取当前系统时间
  27. message = now +":"+ name +":"+ message; // 发送的消息带上当前的时间
  28. OutboundEvent.Builder eventBuilder = new OutboundEvent.Builder();
  29. OutboundEvent event = eventBuilder.name("message")
  30. .mediaType(MediaType.TEXT_PLAIN_TYPE)
  31. .data(String.class, message)
  32. .build();
  33. // 发送广播
  34. broadcaster.broadcast(event);
  35. }
  36. }

其中,SseChatResource 资源类用 @Singleton 注解,告诉 Jersey 运行时,资源类只有一个实例,用于所有传入/sse-chat路径的请求。应用程序引用私有的 broadcaster 字段,这样我们为所有请求可以使用相同的实例。客户端想监听 SSE 事件,先发送 GET 请求到sse-chat的 listenToBroadcast() 资源方法处理。方法创建一个新的 EventOutput 用于展示请求的客户端的连接,并通过 add(EventOutput) 注册 eventOutput 实例到单例 broadcaster。方法返回 eventOutput 导致 Jersey 使请求的客户端事件与 eventOutput 实例绑定,向客户机发送响应 HTTP 头。客户端连接保持开放,客户端等待准备接收新的 SSE 事件。所有的事件通过 broadcaster 写入 eventOutput。这样开发人员可以方便地处理发送新的事件到所有订阅的客户端。

当客户端想要广播新消息给所有的已经监听 SSE 连接的客户端时,它先发送一个 POST 请求将消息内容发到 SseChatResource 资源。 SseChatResource 资源调用方法 broadcastMessage,消息内容作为输入参数。一个新的 SSE 出站事件是建立在标准方法上并传递给 broadcaster。broadcaster 内部在所有注册了的 EventOutput 上调用 write(OutboundEvent) 。当该方法只返回一个标准文本响应给客户端,来通知客户端已经成功广播了消息。正如您可以看到的, broadcastMessage 资源方法只是一个简单的 JAX-RS 资源的方法。

您可能已经注意到,Jersey SseBroadcaster 完成该用例不是强制性的。每个 EventOutput 可以只是存储在收集器里,在 broadcastMessage 方法里面迭代。然而,SseBroadcaster 内部会识别和处理客户端断开连接。当客户端关闭了连接,broadcaster 可检测并删除过期的在内部收集器里面注册了 EventOutput 的连接,以及释放所有服务器端关联了陈旧连接的资源。此外,SseBroadcaster 的实现是线程安全的,这样客户端可以在任何时间连接和断开, SseBroadcaster 总是广播消息给最近收集的注册和活跃的客户端。

客户端代码:

  1. //判断浏览器是否支持 EventSource
  2. if (typeof (EventSource) !== "undefined") {
  3. var source = new EventSource("webapi/sse-chat");
  4. // 当通往服务器的连接被打开
  5. source.onopen = function(event) {
  6. var ta = document.getElementById('response_text');
  7. ta.value = '连接开启!';
  8. };
  9. // 当接收到消息。只能是事件名称是 message
  10. source.onmessage = function(event) {
  11. var ta = document.getElementById('response_text');
  12. ta.value = ta.value + '\n' + event.data;
  13. };
  14. //可以是任意命名的事件名称
  15. /*
  16. source.addEventListener('message', function(event) {
  17. var ta = document.getElementById('response_text');
  18. ta.value = ta.value + '\n' + event.data;
  19. });
  20. */
  21. // 当错误发生
  22. source.onerror = function(event) {
  23. var ta = document.getElementById('response_text');
  24. ta.value = ta.value + '\n' + "连接出错!";
  25. };
  26. } else {
  27. alert("Sorry, your browser does not support server-sent events");
  28. }
  29. function send(message) {
  30. var xmlhttp;
  31. var name = document.getElementById('name_id').value;
  32. if (window.XMLHttpRequest)
  33. {// code for IE7+, Firefox, Chrome, Opera, Safari
  34. xmlhttp=new XMLHttpRequest();
  35. }
  36. else
  37. {// code for IE6, IE5
  38. xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
  39. }
  40. xmlhttp.open("POST","webapi/sse-chat?message=" + message +'&name=' + name ,true);
  41. xmlhttp.send();
  42. }

EventSource 的用法与发布-订阅模式类似。而 send(message) 方法是将消息以 POST 请求发送给服务端,而后将该消息进行广播,从而达到了聊天室的效果。

效果

12. Build Real-Time Web App with SSE 用 SSE 构建实时 Web 应用 - 图10

相关问题

异步请求

报如下错误:

  1. 八月 18, 2015 7:48:28 下午 org.glassfish.jersey.servlet.internal.ResponseWriter suspend
  2. WARNING: Attempt to put servlet request into asynchronous mode has failed. Please check your servlet configuration - all Servlet instances and Servlet filters involved in the request processing must explicitly declare support for asynchronous request processing.
  3. java.lang.IllegalStateException: !asyncSupported
  4. at org.eclipse.jetty.server.Request.startAsync(Request.java:2072)
  5. at org.glassfish.jersey.servlet.async.AsyncContextDelegateProviderImpl$ExtensionImpl.getAsyncContext(AsyncContextDelegateProviderImpl.java:112)
  6. at org.glassfish.jersey.servlet.async.AsyncContextDelegateProviderImpl$ExtensionImpl.suspend(AsyncContextDelegateProviderImpl.java:96)
  7. at org.glassfish.jersey.servlet.internal.ResponseWriter.suspend(ResponseWriter.java:121)
  8. at org.glassfish.jersey.server.ServerRuntime$Responder.writeResponse(ServerRuntime.java:747)
  9. at org.glassfish.jersey.server.ServerRuntime$Responder.processResponse(ServerRuntime.java:424)
  10. at org.glassfish.jersey.server.ServerRuntime$Responder.process(ServerRuntime.java:414)
  11. at org.glassfish.jersey.server.ServerRuntime$2.run(ServerRuntime.java:312)
  12. at org.glassfish.jersey.internal.Errors$1.call(Errors.java:271)
  13. at org.glassfish.jersey.internal.Errors$1.call(Errors.java:267)
  14. at org.glassfish.jersey.internal.Errors.process(Errors.java:315)
  15. at org.glassfish.jersey.internal.Errors.process(Errors.java:297)
  16. at org.glassfish.jersey.internal.Errors.process(Errors.java:267)
  17. at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:317)
  18. at org.glassfish.jersey.server.ServerRuntime.process(ServerRuntime.java:292)
  19. at org.glassfish.jersey.server.ApplicationHandler.handle(ApplicationHandler.java:1139)
  20. at org.glassfish.jersey.servlet.WebComponent.service(WebComponent.java:460)
  21. at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:386)
  22. at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:334)
  23. at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:221)
  24. at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:808)
  25. at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:587)
  26. at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)
  27. at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:577)
  28. at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:223)
  29. at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1127)
  30. at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:515)
  31. at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:185)
  32. at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1061)
  33. at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
  34. at org.eclipse.jetty.server.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:215)
  35. at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:110)
  36. at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:97)
  37. at org.eclipse.jetty.server.Server.handle(Server.java:497)
  38. at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:310)
  39. at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:257)
  40. at org.eclipse.jetty.io.AbstractConnection$2.run(AbstractConnection.java:540)
  41. at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:635)
  42. at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:555)
  43. at java.lang.Thread.run(Thread.java:722)

是指服务器不支持异步请求。解决方法是在 web.xml 中添加

  1. <async-supported>true</async-supported>

最后的 web.xml 为:

  1. <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  2. xmlns="http://xmlns.jcp.org/xml/ns/javaee"
  3. xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
  4. id="WebApp_ID" version="3.1">
  5. <servlet>
  6. <servlet-name>Jersey Web Application</servlet-name>
  7. <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
  8. <init-param>
  9. <param-name>javax.ws.rs.Application</param-name>
  10. <param-value>com.waylau.rest.RestApplication</param-value>
  11. </init-param>
  12. <load-on-startup>1</load-on-startup>
  13. <async-supported>true</async-supported>
  14. </servlet>
  15. <servlet-mapping>
  16. <servlet-name>Jersey Web Application</servlet-name>
  17. <url-pattern>/webapi/*</url-pattern>
  18. </servlet-mapping>
  19. </web-app>

跨域请求

由于浏览器同源策略,凡是发送请求url的协议、域名、端口三者之间任意一与当前页面地址不同即为跨域。

















































URL说明是否允许通信
http://www.a.com/a.js

http://www.a.com/b.js
同一域名下允许
http://www.a.com/lab/a.js

http://www.a.com/script/b.js
同一域名下不同文件夹允许
http://www.a.com:8000/a.js

http://www.a.com/b.js
同一域名,不同端口不允许
http://www.a.com/a.js

https://www.a.com/b.js
同一域名,不同协议不允许
http://www.a.com/a.js

http://70.32.92.74/b.js
域名和域名对应ip不允许
http://www.a.com/a.js

http://script.a.com/b.js
主域相同,子域不同不允许
http://www.a.com/a.js

http://a.com/b.js
同一域名,不同二级域名(同上)不允许(cookie这种情况下也不允许访问)
http://www.cnblogs.com/a.js

http://www.a.com/b.js
不同域名不允许

出于安全考虑,默认是不允许跨域访问的,会报如下异常:

12. Build Real-Time Web App with SSE 用 SSE 构建实时 Web 应用 - 图11

解决是服务器启动 CORS

先是做一个过滤器 CrossDomainFilter.java,将响应头“Access-Control-Allow-Origin”设置为“*”

  1. @Override
  2. public void filter(ContainerRequestContext requestContext,
  3. ContainerResponseContext responseContext) throws IOException {
  4. // 响应头添加了对允许访问的域,* 代表是全部域
  5. responseContext.getHeaders().add("Access-Control-Allow-Origin", "*");
  6. }

在 RestApplication 里,注册该过滤器即可。

  1. public class RestApplication extends ResourceConfig {
  2. public RestApplication() {
  3. // 资源类所在的包路径
  4. packages("com.waylau.rest.resource");
  5. // 注册 MultiPart
  6. register(MultiPartFeature.class);
  7. // 注册CORS过滤器
  8. register(CrossDomainFilter.class);
  9. }
  10. }

这样,就能跨域访问了,如下,192.168.11.103 可以访问 192.168.11.125 站下的资源

12. Build Real-Time Web App with SSE 用 SSE 构建实时 Web 应用 - 图12

源码

sse-real-time-web 项目

参考: